import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { executeCommandInChildProcess } from './command';
import { json } from 'node:stream/consumers';
import { Dirent } from 'node:fs';

async function getLaunchParams(runable: Runable): Promise<{ name: string, description: string, defaultValue: string }[]> {
    let { stdout, } = await executeCommandInChildProcess(`ros2 launch --show-args ${runable.pkg} ${runable.exe}`);
    const result: { name: string, description: string, defaultValue: string }[] = [];

    let lines = stdout.split('\n ').filter(line => !(line.trim().startsWith(' ') || line.trim().endsWith(' '))).map(line => line.trim().replace('  ', ''));
    if (lines.length === 0 || lines.length == 2) {
        return result;
    }
    for (let i = 1; i < lines.length; i += 3) {
        const name = lines[i].replaceAll('\'', '').replaceAll(':', '').trim();
        const description = lines[i + 1].trim();
        const defaultValue = lines[i + 2].split(':')[1].replaceAll('\'', '').replaceAll(')', '').trim();
        result.push({ name, description, defaultValue });
    }
    return result;
}

export class Arguments {
    params: { paramName: string, value: string }[] = [];
    topicRemaps: { original: string, new: string }[] = [];
    constructor(params: { paramName: string, value: string }[] = [], topicRemaps: { original: string, new: string }[] = []) {
        this.params = params;
        this.topicRemaps = topicRemaps;
    }
}

export class Runable {
    pkg: string;
    exe: string;
    constructor(pkg: string, exe: string) {
        this.pkg = pkg;
        this.exe = exe;
    }

    asKey(): string {
        return `${this.pkg}:${this.exe}`;
    }
}

class TreeNode extends vscode.TreeItem {
    constructor(label: string, collapsed: vscode.TreeItemCollapsibleState, contextValue: string, tooltip?: string, iconPath?: string, description?: string | boolean, public readonly path?: string) {
        super(label, collapsed);
        this.tooltip = tooltip;
        this.iconPath = iconPath;
        this.contextValue = contextValue;
        this.description = description;
        if (this.path) {
            this.command = {
                command: 'ros2-helper.open_file',
                title: 'Open',
                arguments: [vscode.Uri.file(this.path)]
            };
        }
    }
}
export enum ExecutableType {
    Bin,
    Script
}

export class Ros2Package extends TreeNode {
    pkgName: string;
    children: TreeNode[];
    constructor(name: string, children?: TreeNode[]) {
        let iconPath = path.join(__dirname, '../resource/package.png');
        super(name, vscode.TreeItemCollapsibleState.Collapsed, 'ros2Package', `Package: ${name}`, iconPath);
        this.children = children || [];
        this.pkgName = name;
    }

    packageName(): string {
        return this.pkgName;
    }
}

export class Ros2Executable extends TreeNode {
    type: ExecutableType;
    exeName: string;
    relatePackage: string;
    params: ParamsNode[];
    remaps: TopicRemapNode[];
    constructor(name: string, relatePackage: string, type: ExecutableType, description?: string, args?: Arguments, executablePath?: string) {
        let iconPath;
        switch (type) {
            case ExecutableType.Bin:
                iconPath = path.join(__dirname, '../resource/bin.png');
                break;
            case ExecutableType.Script:
                iconPath = path.join(__dirname, '../resource/python.png');
                break;
        }
        let hasArg = args != undefined && (args.params.length > 0 || args.topicRemaps.length > 0);
        super(name, hasArg ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, 'ros2Executable', description, iconPath, false, executablePath);
        this.type = type;
        this.relatePackage = relatePackage;
        this.exeName = name;
        this.params = args?.params.map(param => new ParamsNode(param.paramName, param.value, this)) || [];
        this.remaps = args?.topicRemaps.map(remap => new TopicRemapNode(remap.original, remap.new, this)) || [];
    }
    executableName(): string {
        return this.exeName;
    }
    packageName(): string {
        return this.relatePackage;
    }
}

export class Ros2LaunchFile extends TreeNode {
    fileName: string;
    relatePackage: string;
    params: ParamsNode[] = [];
    constructor(name: string, relatePackage: string, description?: string, launchFilePath?: string) {
        let iconPath = path.join(__dirname, '../resource/launch.png');
        super(name, vscode.TreeItemCollapsibleState.Collapsed, 'ros2LaunchFile', description, iconPath, false, launchFilePath);
        this.relatePackage = relatePackage;
        this.fileName = name;
    }
    launchFileName(): string {
        return this.fileName;
    }
    packageName(): string {
        return this.relatePackage;
    }
}

export class ParamsNode extends TreeNode {
    parent: TreeNode;
    name: string;
    value: string;
    constructor(name: string, value: string, parent: TreeNode, desc?: string) {
        super(name, vscode.TreeItemCollapsibleState.None, 'param', desc, path.join(__dirname, '../resource/params.png'), value);
        this.parent = parent;
        this.name = name;
        this.value = value;
    }

    paramName(): string {
        return this.name;
    }
    paramValue(): string {
        return this.value;
    }
}

export class TopicRemapNode extends TreeNode {
    parent: TreeNode;
    original: string;
    newTop: string;
    constructor(original: string, newTopic: string, parent: TreeNode) {
        super(original, vscode.TreeItemCollapsibleState.None, 'remap', `${original}:=${newTopic}`, path.join(__dirname, '../resource/topic.png'), newTopic);
        this.parent = parent;
        this.original = original;
        this.newTop = newTopic;
    }

    originalTopic(): string {
        return this.original;
    }
    newTopic(): string {
        return this.newTop;
    }
}

class PlaceHolderItem extends TreeNode {
    constructor() {
        super('empty', vscode.TreeItemCollapsibleState.None, "placeholder");
    }
}

export class ROS2ExecutableProvider implements vscode.TreeDataProvider<TreeNode> {
    private _onDidChangeTreeData: vscode.EventEmitter<TreeNode | undefined | null> = new vscode.EventEmitter<TreeNode | undefined | null>();
    readonly onDidChangeTreeData: vscode.Event<TreeNode | undefined | null> = this._onDidChangeTreeData.event;
    datas: Map<string, Arguments>;
    context: vscode.ExtensionContext;
    private rootNodes: TreeNode[] = [];

    async save() {
        const array: { key: string; value: Arguments }[] = [];
        this.datas.forEach((value, key) => {
            array.push({ key, value });
        });
        await this.context.workspaceState.update('ros2-helper.args', array);
        console.log('save resourceTree');
    }

    refresh() {
        console.log('refresh resourceTree');
        this._onDidChangeTreeData.fire(undefined);
    }
    reset() {
        console.log('reset record for all params and topic remaps');
        this.datas.clear();
        this.save();
        this.refresh();
    }


    isEmptyObject(obj: any): boolean {
        return Object.keys(obj).length === 0 && obj.constructor === Object;
    }

    constructor(private workspaceRoot: string, context: vscode.ExtensionContext) {
        this.workspaceRoot = workspaceRoot;
        this.context = context;
        let array: { key: string; value: Arguments }[] | undefined = context.workspaceState.get<{ key: string; value: Arguments }[]>('ros2-helper.args');
        if (array === undefined || this.isEmptyObject(array)) {
            this.datas = new Map<string, Arguments>();
            return;
        }
        this.datas = new Map(array.map(item => [item.key, item.value]));
    }

    async getPackageNames(): Promise<string[]> {
        let packageNames = [];
        for (let i = 0; i < this.rootNodes.length; i++) {
            if (this.rootNodes[i] instanceof Ros2Package) {
                packageNames.push((this.rootNodes[i] as Ros2Package).packageName());
            }
        }
        return packageNames;
    }

    getTreeItem(element: TreeNode): vscode.TreeItem {
        return element;
    }

    getChildren(element?: TreeNode): Thenable<TreeNode[]> {
        if (!element) {
            return this.scanInstallDirectory();
        }
        if (element instanceof Ros2Package) {
            return Promise.resolve(element.children);
        }
        if (element instanceof Ros2Executable) {
            let params = element.params as TreeNode[];
            let remaps = element.remaps as TreeNode[];
            let paramsAndRemaps = params.concat(remaps);
            if (paramsAndRemaps.length == 0) {
                return Promise.resolve([new PlaceHolderItem()]);
            }
            return Promise.resolve(paramsAndRemaps);
        }
        if (element instanceof Ros2LaunchFile) {
            return this.getLaunchParamsNode(element);
        }
        else {
            return Promise.resolve([]);
        }
    }
    private async scanInstallDirectory(): Promise<TreeNode[]> {
        const installDir = path.join(this.workspaceRoot, 'install');
        let src_path = vscode.workspace.getConfiguration().get<string>('ros2-helper.src_path');
        src_path = src_path ? src_path : 'src';
        const srcDir = path.join(this.workspaceRoot, src_path);
        if (!fs.existsSync(installDir)) {
            return [];
        }

        const dirs = await fs.promises.readdir(installDir, { withFileTypes: true });
        let rootNodes = dirs
            .filter(dirent => dirent.isDirectory())
            .map(async (dirent) => {
                const installPackageDir = path.join(installDir, dirent.name);
                const srcPackageDir = path.join(srcDir, dirent.name);
                const packageItems = await this.scanPackageDirectory(installPackageDir, srcPackageDir);
                return new Ros2Package(dirent.name, packageItems);
            });
        this.rootNodes = await Promise.all(rootNodes);
        return Promise.all(rootNodes);
    }

    private async scanPackageDirectory(installDir: string, srcDir: string): Promise<TreeNode[]> {
        const binDir = path.join(installDir, 'lib', path.basename(installDir));
        const launchDir = path.join(installDir, 'share', path.basename(installDir), 'launch');
        const packageName = path.basename(installDir);

        const items: TreeNode[] = [];

        if (fs.existsSync(binDir)) {
            const executables = await fs.promises.readdir(binDir, { withFileTypes: true });
            items.push(...executables
                .filter(dirent => (dirent.isSymbolicLink() || dirent.isFile()) && this.isBinaryFile(path.join(binDir, dirent.name)))
                .map(dirent => new Ros2Executable(dirent.name, packageName, ExecutableType.Bin, `cpp node: ${dirent.name}`,
                    this.datas.get(new Runable(packageName, dirent.name).asKey()) || new Arguments())));
            items.push(...executables
                .filter(dirent => (dirent.isSymbolicLink() || dirent.isFile()) && (!this.isBinaryFile(path.join(binDir, dirent.name))))
                .map(dirent => new Ros2Executable(dirent.name, packageName, ExecutableType.Script, `python node: ${dirent.name}`,
                    this.datas.get(new Runable(packageName, dirent.name).asKey()) || new Arguments(), path.join(srcDir, packageName, dirent.name) + '.py')));
        }

        if (fs.existsSync(launchDir)) {
            const launchFiles = await fs.promises.readdir(launchDir, { withFileTypes: true });
            items.push(...launchFiles
                .filter(dirent => (dirent.isSymbolicLink() || dirent.isFile()))
                .map(dirent => new Ros2LaunchFile(dirent.name, packageName, path.join(srcDir, "launch", dirent.name))));
        }

        return items;
    }

    private async getLaunchParamsNode(launch: Ros2LaunchFile): Promise<TreeNode[]> {
        let runable = new Runable(launch.packageName(), launch.launchFileName());
        let nodes = (await getLaunchParams(runable))
            .map(param => {
                let Arguments = this.datas.get(runable.asKey());
                if (!Arguments) {
                    return new ParamsNode(param.name, param.defaultValue, launch, param.description)
                }
                let index = Arguments.params.findIndex(p => p.paramName === param.name);
                if (index == -1) {
                    return new ParamsNode(param.name, param.defaultValue, launch, param.description)
                }
                return new ParamsNode(param.name, Arguments.params[index].value, launch, param.description)
            });
        launch.params = nodes;
        if (nodes.length == 0) {
            return [new PlaceHolderItem()];
        }
        return nodes;
    }

    private isBinaryFile(filePath: string): boolean {
        const buffer = fs.readFileSync(filePath, { encoding: null });
        const sample = buffer.slice(0, 1024);
        for (let i = 0; i < sample.length; i++) {
            const byte = sample[i];
            if (byte === 0 || byte > 127) {
                return true;
            }
        }
        return false;
    }
}